﻿/**
* writeCapture.js v1.0.5
*
* @author noah <noah.sloan@gmail.com>
*
*/
(function ($, global) {
    var doc = global.document;
    function doEvil(code) {
        var div = doc.createElement('div');
        doc.body.insertBefore(div, null);
        $.replaceWith(div, '<script type="text/javascript">' + code + '</script>');
    }
    // ensure we have our support functions
    $ = $ || (function (jQuery) {
        /**
        * @name writeCaptureSupport
        *
        * The support functions writeCapture needs.
        */
        return {
            /**
            * Takes an options parameter that must support the following:
            * {
            * 	url: url,
            * 	type: 'GET', // all requests are GET
            * 	dataType: "script", // it this is set to script, script tag injection is expected, otherwise, treat as plain text
            * 	async: true/false, // local scripts are loaded synchronously by default
            * 	success: callback(text,status), // must not pass a truthy 3rd parameter
            * 	error: callback(xhr,status,error) // must pass truthy 3rd parameter to indicate error
            * }
            */
            ajax: jQuery.ajax,
            /**
            * @param {String Element} selector an Element or selector
            * @return {Element} the first element matching selector
            */
            $: function (s) { return jQuery(s)[0]; },
            /**
            * @param {String jQuery Element} selector the element to replace.
            * writeCapture only needs the first matched element to be replaced.
            * @param {String} content the content to replace
            * the matched element with. script tags must be evaluated/loaded
            * and executed if present.
            */
            replaceWith: function (selector, content) {
                // jQuery 1.4? has a bug in replaceWith so we can't use it directly
                var el = jQuery(selector)[0];
                var next = el.nextSibling, parent = el.parentNode;

                jQuery(el).remove();

                if (next) {
                    jQuery(next).before(content);
                } else {
                    jQuery(parent).append(content);
                }
            },

            onLoad: function (fn) {
                jQuery(fn);
            },

            copyAttrs: function (src, dest) {
                var el = jQuery(dest), attrs = src.attributes;
                for (var i = 0, len = attrs.length; i < len; i++) {
                    if (attrs[i] && attrs[i].value) {
                        try {
                            el.attr(attrs[i].name, attrs[i].value);
                        } catch (e) { }
                    }
                }
            }
        };
    })(global.jQuery);

    $.copyAttrs = $.copyAttrs || function () { };
    $.onLoad = $.onLoad || function () {
        throw "error: autoAsync cannot be used without jQuery " +
			"or defining writeCaptureSupport.onLoad";
    };

    // utilities
    function each(array, fn) {
        for (var i = 0, len = array.length; i < len; i++) {
            if (fn(array[i]) === false) return;
        }
    }
    function isFunction(o) {
        return Object.prototype.toString.call(o) === "[object Function]";
    }
    function isString(o) {
        return Object.prototype.toString.call(o) === "[object String]";
    }
    function slice(array, start, end) {
        return Array.prototype.slice.call(array, start || 0, end || array && array.length);
    }
    function any(array, fn) {
        var result = false;
        each(array, check);
        function check(it) {
            return !(result = fn(it));
        }
        return result;
    }

    function SubQ(parent) {
        this._queue = [];
        this._children = [];
        this._parent = parent;
        if (parent) parent._addChild(this);
    }

    SubQ.prototype = {
        _addChild: function (q) {
            this._children.push(q);
        },
        push: function (task) {
            this._queue.push(task);
            this._bubble('_doRun');
        },
        pause: function () {
            this._bubble('_doPause');
        },
        resume: function () {
            this._bubble('_doResume');
        },
        _bubble: function (name) {
            var root = this;
            while (!root[name]) {
                root = root._parent;
            }
            return root[name]();
        },
        _next: function () {
            if (any(this._children, runNext)) return true;
            function runNext(c) {
                return c._next();
            }
            var task = this._queue.shift();
            if (task) {
                task();
            }
            return !!task;
        }
    };

    /**
    * Provides a task queue for ensuring that scripts are run in order.
    *
    * The only public methods are push, pause and resume.
    */
    function Q(parent) {
        if (parent) {
            return new SubQ(parent);
        }
        SubQ.call(this);
        this.paused = 0;
    }

    Q.prototype = (function () {
        function f() { }
        f.prototype = SubQ.prototype;
        return new f();
    })();

    Q.prototype._doRun = function () {
        if (!this.running) {
            this.running = true;
            try {
                // just in case there is a bug, always resume
                // if paused is less than 1
                while (this.paused < 1 && this._next()) { }
            } finally {
                this.running = false;
            }
        }
    };
    Q.prototype._doPause = function () {
        this.paused++;
    };
    Q.prototype._doResume = function () {
        this.paused--;
        this._doRun();
    };

    // TODO unit tests...
    function MockDocument() { }
    MockDocument.prototype = {
        _html: '',
        open: function () {
            this._opened = true;
            if (this._delegate) {
                this._delegate.open();
            }
        },
        write: function (s) {
            if (this._closed) return;
            this._written = true;
            if (this._delegate) {
                this._delegate.write(s);
            } else {
                this._html += s;
            }
        },
        writeln: function (s) {
            this.write(s + '\n');
        },
        close: function () {
            this._closed = true;
            if (this._delegate) {
                this._delegate.close();
            }
        },
        copyTo: function (d) {
            this._delegate = d;
            d.foobar = true;
            if (this._opened) {
                d.open();
            }
            if (this._written) {
                d.write(this._html);
            }
            if (this._closed) {
                d.close();
            }
        }
    };

    // test for IE 6/7 issue (issue 6) that prevents us from using call
    var canCall = (function () {
        var f = { f: doc.getElementById };
        try {
            f.f.call(doc, 'abc');
            return true;
        } catch (e) {
            return false;
        }
    })();

    function unProxy(elements) {
        each(elements, function (it) {
            var real = doc.getElementById(it.id);
            if (!real) {
                logError('<proxyGetElementById - finish>',
					'no element in writen markup with id ' + it.id);
                return;
            }

            each(it.el.childNodes, function (it) {
                real.appendChild(it);
            });

            if (real.contentWindow) {
                // TODO why is the setTimeout necessary?
                global.setTimeout(function () {
                    it.el.contentWindow.document.
						copyTo(real.contentWindow.document);
                }, 1);
            }
            $.copyAttrs(it.el, real);
        });
    }

    function getOption(name, options) {
        if (options && options[name] === false) {
            return false;
        }
        return options && options[name] || self[name];
    }

    function capture(context, options) {
        var tempEls = [],
			proxy = getOption('proxyGetElementById', options),
			forceLast = getOption('forceLastScriptTag', options),
			writeOnGet = getOption('writeOnGetElementById', options),
			immediate = getOption('immediateWrites', options),
			state = {
			    write: doc.write,
			    writeln: doc.writeln,
			    finish: function () { },
			    out: ''
			};
        context.state = state;
        doc.write = immediate ? immediateWrite : replacementWrite;
        doc.writeln = immediate ? immediateWriteln : replacementWriteln;
        if (proxy || writeOnGet) {
            state.getEl = doc.getElementById;
            doc.getElementById = getEl;
            if (writeOnGet) {
                findEl = writeThenGet;
            } else {
                findEl = makeTemp;
                state.finish = function () {
                    unProxy(tempEls);
                };
            }
        }
        if (forceLast) {
            state.getByTag = doc.getElementsByTagName;
            doc.getElementsByTagName = function (name) {
                var result = slice(canCall ? state.getByTag.call(doc, name) :
					state.getByTag(name));
                if (name === 'script') {
                    result.push($.$(context.target));
                }
                return result;
            };
            var f = state.finish;
            state.finish = function () {
                f();
                doc.getElementsByTagName = state.getByTag;
            };
        }
        function replacementWrite(s) {
            state.out += s;
        }
        function replacementWriteln(s) {
            state.out += s + '\n';
        }
        function immediateWrite(s) {
            var target = $.$(context.target);
            var div = doc.createElement('div');
            target.parentNode.insertBefore(div, target);
            $.replaceWith(div, sanitize(s));
        }
        function immediateWriteln(s) {
            var target = $.$(context.target);
            var div = doc.createElement('div');
            target.parentNode.insertBefore(div, target);
            $.replaceWith(div, sanitize(s) + '\n');
        }
        function makeTemp(id) {
            var t = doc.createElement('div');
            tempEls.push({ id: id, el: t });
            // mock contentWindow in case it's supposed to be an iframe
            t.contentWindow = { document: new MockDocument() };
            return t;
        }
        function writeThenGet(id) {
            var target = $.$(context.target);
            var div = doc.createElement('div');
            target.parentNode.insertBefore(div, target);
            $.replaceWith(div, state.out);
            state.out = '';
            return canCall ? state.getEl.call(doc, id) :
				state.getEl(id);
        }
        function getEl(id) {
            var result = canCall ? state.getEl.call(doc, id) :
				state.getEl(id);
            return result || findEl(id);
        }
        return state;
    }
    function uncapture(state) {
        doc.write = state.write;
        doc.writeln = state.writeln;
        if (state.getEl) {
            doc.getElementById = state.getEl;
        }
        return state.out;
    }

    function clean(code) {
        // IE will execute inline scripts with <!-- (uncommented) on the first
        // line, but will not eval() them happily
        return code && code.replace(/^\s*<!(\[CDATA\[|--)/, '').replace(/(\]\]|--)>\s*$/, '');
    }

    function ignore() { }
    function doLog(code, error) {
        console.error("Error", error, "executing code:", code);
    }

    var logError = isFunction(global.console && console.error) ?
			doLog : ignore;

    function captureWrite(code, context, options) {
        var state = capture(context, options);
        try {
            doEvil(clean(code));
        } catch (e) {
            logError(code, e);
        } finally {
            uncapture(state);
        }
        return state;
    }

    // copied from jQuery
    function isXDomain(src) {
        var parts = /^(\w+:)?\/\/([^\/?#]+)/.exec(src);
        return parts && (parts[1] && parts[1] != location.protocol || parts[2] != location.host);
    }

    function attrPattern(name) {
        return new RegExp('[\\s\\r\\n]' + name + '=(?:(["\'])([\\s\\S]*?)\\1|([^\\s>]+))', 'i');
    }

    function matchAttr(name) {
        var regex = attrPattern(name);
        return function (tag) {
            var match = regex.exec(tag) || [];
            return match[2] || match[3];
        };
    }

    var SCRIPT_TAGS = /(<script[^>]*>)([\s\S]*?)<\/script>/ig,
		SCRIPT_2 = /<script[^>]*\/>/ig,
		SRC_REGEX = attrPattern('src'),
		SRC_ATTR = matchAttr('src'),
		TYPE_ATTR = matchAttr('type'),
		LANG_ATTR = matchAttr('language'),
		GLOBAL = "__document_write_ajax_callbacks__",
		DIV_PREFIX = "__document_write_ajax_div-",
		TEMPLATE = "window['" + GLOBAL + "']['%d']();",
		callbacks = global[GLOBAL] = {},
		TEMPLATE_TAG = '<script type="text/javascript">' + TEMPLATE + '</script>',
		global_id = 0;
    function nextId() {
        return (++global_id).toString();
    }

    function normalizeOptions(options, callback) {
        var done;
        if (isFunction(options)) {
            done = options;
            options = null;
        }
        options = options || {};
        done = done || options && options.done;
        options.done = callback ? function () {
            callback(done);
        } : done;
        return options;
    }

    // The global Q synchronizes all sanitize operations.
    // The only time this synchronization is really necessary is when two or
    // more consecutive sanitize operations make async requests. e.g.,
    // sanitize call A requests foo, then sanitize B is called and bar is
    // requested. document.write was replaced by B, so if A returns first, the
    // content will be captured by B, then when B returns, document.write will
    // be the original document.write, probably messing up the page. At the
    // very least, A will get nothing and B will get the wrong content.
    var GLOBAL_Q = new Q();

    var debug = [];
    var logDebug = window._debugWriteCapture ? function () { } :
		function (type, src, data) {
		    debug.push({ type: type, src: src, data: data });
		};

    var logString = window._debugWriteCapture ? function () { } :
		function () {
		    debug.push(arguments);
		};

    function newCallback(fn) {
        var id = nextId();
        callbacks[id] = function () {
            fn();
            delete callbacks[id];
        };
        return id;
    }

    function newCallbackTag(fn) {
        return TEMPLATE_TAG.replace(/%d/, newCallback(fn));
    }

    /**
    * Sanitize the given HTML so that the scripts will execute with a modified
    * document.write that will capture the output and append it in the
    * appropriate location.
    *
    * @param {String} html
    * @param {Object Function} [options]
    * @param {Function} [options.done] Called when all the scripts in the
    * sanitized HTML have run.
    * @param {boolean} [options.asyncAll] If true, scripts loaded from the
    * same domain will be loaded asynchronously. This can improve UI
    * responsiveness, but will delay completion of the scripts and may
    * cause problems with some scripts, so it defaults to false.
    */
    function sanitize(html, options, parentQ, parentContext) {
        // each HTML fragment has it's own queue
        var queue = parentQ && new Q(parentQ) || GLOBAL_Q;
        options = normalizeOptions(options);
        var done = getOption('done', options);
        var doneHtml = '';

        var fixUrls = getOption('fixUrls', options);
        if (!isFunction(fixUrls)) {
            fixUrls = function (src) { return src; };
        }

        // if a done callback is passed, append a script to call it
        if (isFunction(done)) {
            // no need to proxy the call to done, so we can append this to the
            // filtered HTML
            doneHtml = newCallbackTag(function () {
                queue.push(done);
            });
        }
        // for each tag, generate a function to load and eval the code and queue
        // themselves
        return html.replace(SCRIPT_TAGS, proxyTag).replace(SCRIPT_2, proxyBodyless) + doneHtml;
        function proxyBodyless(tag) {
            // hack in a bodyless tag...
            return proxyTag(tag, tag.substring(0, tag.length - 2) + '>', '');
        }
        function proxyTag(element, openTag, code) {
            var src = SRC_ATTR(openTag),
				type = TYPE_ATTR(openTag) || '',
				lang = LANG_ATTR(openTag) || '',
				isJs = (!type && !lang) || // no type or lang assumes JS
					type.toLowerCase().indexOf('javascript') !== -1 ||
					lang.toLowerCase().indexOf('javascript') !== -1;

            logDebug('replace', src, element);

            if (!isJs) {
                return element;
            }

            var id = newCallback(queueScript), divId = DIV_PREFIX + id,
				run, context = { target: '#' + divId, parent: parentContext };

            function queueScript() {
                queue.push(run);
            }

            if (src) {
                // fix for the inline script that writes a script tag with encoded
                // ampersands hack (more comon than you'd think)
                src = fixUrls(src);

                openTag = openTag.replace(SRC_REGEX, '');
                if (isXDomain(src)) {
                    // will load async via script tag injection (eval()'d on
                    // it's own)
                    run = loadXDomain;
                } else {
                    // can be loaded then eval()d
                    if (getOption('asyncAll', options)) {
                        run = loadAsync();
                    } else {
                        run = loadSync;
                    }
                }
            } else {
                // just eval code and be done
                run = runInline;

            }
            function runInline() {
                captureHtml(code);
            }
            function loadSync() {
                $.ajax({
                    url: src,
                    type: 'GET',
                    dataType: 'text',
                    async: false,
                    success: function (html) {
                        captureHtml(html);
                    }
                });
            }
            function logAjaxError(xhr, status, error) {
                logError("<XHR for " + src + ">", error);
                queue.resume();
            }
            function setupResume() {
                return newCallbackTag(function () {
                    queue.resume();
                });
            }
            function loadAsync() {
                var ready, scriptText;
                function captureAndResume(script, status) {
                    if (!ready) {
                        // loaded before queue run, cache text
                        scriptText = script;
                        return;
                    }
                    try {
                        captureHtml(script, setupResume());
                    } catch (e) {
                        logError(script, e);
                    }
                }
                // start loading the text
                $.ajax({
                    url: src,
                    type: 'GET',
                    dataType: 'text',
                    async: true,
                    success: captureAndResume,
                    error: logAjaxError
                });
                return function () {
                    ready = true;
                    if (scriptText) {
                        // already loaded, so don't pause the queue and don't resume!
                        captureHtml(scriptText);
                    } else {
                        queue.pause();
                    }
                };
            }
            function loadXDomain(cb) {
                var state = capture(context, options);
                queue.pause(); // pause the queue while the script loads
                logDebug('pause', src);

                doXDomainLoad(context.target, src, captureAndResume);

                function captureAndResume(xhr, st, error) {
                    logDebug('out', src, state.out);
                    html(uncapture(state),
						newCallbackTag(state.finish) + setupResume());
                    logDebug('resume', src);
                }
            }
            function captureHtml(script, cb) {
                var state = captureWrite(script, context, options);
                cb = newCallbackTag(state.finish) + (cb || '');
                html(state.out, cb);
            }
            function safeOpts(options) {
                var copy = {};
                for (var i in options) {
                    if (options.hasOwnProperty(i)) {
                        copy[i] = options[i];
                    }
                }
                delete copy.done;
                return copy;
            }
            function html(markup, cb) {
                $.replaceWith(context.target, sanitize(markup, safeOpts(options), queue, context) + (cb || ''));
            }
            return '<div style="display: none" id="' + divId + '"></div>' + openTag +
				TEMPLATE.replace(/%d/, id) + '</script>';
        }
    }

    function doXDomainLoad(target, url, success) {
        // TODO what about scripts that fail to load? bad url, etc.?
        var script = document.createElement("script");
        script.src = url;

        target = $.$(target);

        var done = false, parent = target.parentNode;

        // Attach handlers for all browsers
        script.onload = script.onreadystatechange = function () {
            if (!done && (!this.readyState ||
					this.readyState == "loaded" || this.readyState == "complete")) {
                done = true;
                success();

                // Handle memory leak in IE
                script.onload = script.onreadystatechange = null;
                parent.removeChild(script);
            }
        };

        parent.insertBefore(script, target);
    }

    /**
    * Sanitizes all the given fragments and calls action with the HTML.
    * The next fragment is not started until the previous fragment
    * has executed completely.
    *
    * @param {Array} fragments array of objects like this:
    * {
    *   html: '<p>My html with a <script...',
    *   action: function(safeHtml,frag) { doSomethingToInject(safeHtml); },
    *   options: {} // optional, see #sanitize
    * }
    * Where frag is the object.
    *
    * @param {Function} [done] Optional. Called when all fragments are done.
    */
    function sanitizeSerial(fragments, done) {
        // create a queue for these fragments and make it the parent of each
        // sanitize call
        var queue = GLOBAL_Q;
        each(fragments, function (f) {
            queue.push(run);
            function run() {
                f.action(sanitize(f.html, f.options, queue), f);
            }
        });
        if (done) {
            queue.push(done);
        }
    }

    function findLastChild(el) {
        var n = el;
        while (n && n.nodeType === 1) {
            el = n;
            n = n.lastChild;
            // last child may not be an element
            while (n && n.nodeType !== 1) {
                n = n.previousSibling;
            }
        }
        return el;
    }

    /**
    * Experimental - automatically captures document.write calls and
    * defers them untill after page load.
    * @param {Function} [done] optional callback for when all the
    * captured content has been loaded.
    */
    function autoCapture(done) {
        var write = doc.write,
			writeln = doc.writeln,
			currentScript,
			autoQ = [];
        doc.writeln = function (s) {
            doc.write(s + '\n');
        };
        var state;
        doc.write = function (s) {
            var scriptEl = findLastChild(doc.body);
            if (scriptEl !== currentScript) {
                currentScript = scriptEl;
                autoQ.push(state = {
                    el: scriptEl,
                    out: []
                });
            }
            state.out.push(s);
        };
        $.onLoad(function () {
            // for each script, append a div immediately after it,
            // then replace the div with the sanitized output
            var el, div, out, safe, doneFn;
            done = normalizeOptions(done);
            doneFn = done.done;
            done.done = function () {
                doc.write = write;
                doc.writeln = writeln;
                if (doneFn) doneFn();
            };
            for (var i = 0, len = autoQ.length; i < len; i++) {
                el = autoQ[i].el;
                div = doc.createElement('div');
                el.parentNode.insertBefore(div, el.nextSibling);
                out = autoQ[i].out.join('');
                // only the last snippet gets passed the callback
                safe = len - i === 1 ? sanitize(out, done) : sanitize(out);
                $.replaceWith(div, safe);
            }
        });
    }

    function extsrc(cb) {
        var scripts = document.getElementsByTagName('script'),
			s, o, html, q, ext, async, doneCount = 0,
			done = cb ? newCallbackTag(function () {
			    if (++doneCount >= exts.length) {
			        cb();
			    }
			}) : '',
			exts = [];

        for (var i = 0, len = scripts.length; i < len; i++) {
            s = scripts[i];
            ext = s.getAttribute('extsrc');
            async = s.getAttribute('asyncsrc');
            if (ext || async) {
                exts.push({ ext: ext, async: async, s: s });
            }
        }

        for (i = 0, len = exts.length; i < len; i++) {
            o = exts[i];
            if (o.ext) {
                html = '<script type="text/javascript" src="' + o.ext + '"> </script>';
                $.replaceWith(o.s, sanitize(html) + done);
            } else if (o.async) {
                html = '<script type="text/javascript" src="' + o.async + '"> </script>';
                $.replaceWith(o.s, sanitize(html, { asyncAll: true }, new Q()) + done);
            }
        }
    }

    var name = 'writeCapture';
    var self = global[name] = {
        _original: global[name],
        support: $,
        /**
        */
        fixUrls: function (src) {
            return src.replace(/&amp;/g, '&');
        },
        noConflict: function () {
            global[name] = this._original;
            return this;
        },
        debug: debug,
        /**
        * Enables a fun little hack that replaces document.getElementById and
        * creates temporary elements for the calling code to use.
        */
        proxyGetElementById: false,
        // this is only for testing, please don't use these
        _forTest: {
            Q: Q,
            GLOBAL_Q: GLOBAL_Q,
            $: $,
            matchAttr: matchAttr,
            slice: slice,
            capture: capture,
            uncapture: uncapture,
            captureWrite: captureWrite
        },
        replaceWith: function (selector, content, options) {
            $.replaceWith(selector, sanitize(content, options));
        },
        html: function (selector, content, options) {
            var el = $.$(selector);
            el.innerHTML = '<span/>';
            $.replaceWith(el.firstChild, sanitize(content, options));
        },
        load: function (selector, url, options) {
            $.ajax({
                url: url,
                dataType: 'text',
                type: "GET",
                success: function (content) {
                    self.html(selector, content, options);
                }
            });
        },
        extsrc: extsrc,
        autoAsync: autoCapture,
        sanitize: sanitize,
        sanitizeSerial: sanitizeSerial
    };

})(this.writeCaptureSupport, this);